================================== Extending RISE through paradigms ================================== This chapter describes how RISE separates *what* a model is from *how* it is solved, through the notion of a **paradigm**. A paradigm is a self-contained computational strategy -- perturbation, recursive local linearization (RLL), :math:`k`-th order Taylor projection (TP), occasionally-binding-constraint (occbin) piecewise linearization, and so on -- that plugs into mainland RISE through a small, fixed interface. The aim is to add advanced solution methods **without modifying RISE's core** and without paradigms interfering with one another. This chapter is primarily for developers extending RISE. It first describes what a paradigm is and the interface a paradigm must satisfy, and only then gives the concrete paradigms that ship with RISE as worked examples. Three paradigms are currently provided (each under ``paradigms/``): #. **occbin** -- piecewise-linear, OccBin-style occasionally-binding constraints (``occbin.paradigm``); #. **recursive local linearization (RLL)** (``rll.paradigm``); #. :math:`k`-**th order Taylor projection (TP)** (``rtp.paradigm``). Each is described in `Examples`_, after the general interface. Goals ===== The paradigm architecture is designed to deliver: - a clean separation between model *structure* and computational *method*; - support for multiple parameterizations of the same model; - extensibility to advanced methods without touching RISE core; - prevention of cross-talk between parameterizations; - strictly read-only access to RISE internals by paradigm code. A model contains **exactly one paradigm** at a time, but may carry **multiple parameter vectors**, each producing its own solution object. What a paradigm is ================== A paradigm :math:`\mathcal{P}` is a computational universe that #. solves a parameterized model :math:`m(\theta)`, #. defines the internal representation of the solution :math:`S(\theta)`, and #. provides operators acting on that solution (e.g. a one-step forecast). It is **not** the model, **not** a parameter vector, and **not** a container for multiple solutions. Formally, .. math:: S(\theta) = \mathcal{P}.\texttt{solve\_engine}\bigl(m(\theta),\, \texttt{regular\_solver}\bigr), where :math:`S(\theta)` is *partly transparent* to RISE (through the standard envelope below) and *partly opaque* (through the ``S.solution`` payload). Responsibilities of a paradigm ============================== A paradigm **must**: - operate on a single parameter vector at a time; - return a single solution object ``S`` conforming to the standard envelope; - provide operators derived from that solution; - remain unaware of parameter multiplicity; - treat the model object ``M`` as **read-only** -- it may call ``regular_solver(M, ...)`` to obtain derivatives, but must never modify ``M`` nor return a modified ``M`` to RISE; - avoid modifying RISE core structures or global options. A paradigm **may** use RISE's baseline solver internally, use RISE's constraint enforcement, and keep mutable internal state inside ``S.solution`` (see `Mutable solution objects`_). A paradigm **must not** store global state across parameterizations, return a modified ``M``, or manage arrays of solutions. The standard solution envelope ============================== Every paradigm's ``solve_engine`` returns a struct ``S`` with the fields below. This *standard envelope* lets RISE orchestrate the workflow (convergence checking, re-solve decisions, filter routing) **without knowing anything about the paradigm's internals**. .. list-table:: :header-rows: 1 :widths: 18 18 64 * - Field - Type - Meaning * - ``is_solved`` - ``[1x2 logical]`` - ``[ss_ok, dynamics_ok]``: first entry, the steady state / baseline solve succeeded; second entry, the dynamics / projection solve succeeded. * - ``warrant_resolving`` - ``logical`` - ``true`` if the solve failed or degraded and RISE should attempt a re-solve. * - ``retcode`` - ``integer`` - ``0`` = success (RISE's usual ``retcode`` convention). * - ``H`` - matrix or ``[]`` - measurement-error covariance for the Kalman filter; ``[]`` if the paradigm provides none. * - ``solution`` - handle object - opaque, paradigm-owned payload. RISE stores it but never inspects it. All paradigm-specific state lives here. The envelope is deliberately minimal. Fields specific to perturbation theory (eigenvalues, number of solutions, ``Tz``, ...) are **not** required, because they are meaningless or misleading for paradigms such as RLL and TP; those paradigms keep such quantities inside ``S.solution``. A paradigm should start from a *failed* default and populate fields as each stage succeeds, so partial failures are reported correctly:: function S = make_envelope() S.is_solved = [false, false]; S.warrant_resolving = true; S.retcode = 1; S.H = []; S.solution = []; end The minimum interface contract ============================== A paradigm exposes an ``engines`` struct with these fields. **name** (required) A short string identifier, e.g. ``'perturbation'``, ``'rll'``, ``'rtp'``. **solve_engine** (required) :: S = solve_engine(M, regular_solver) ``M`` is **read-only**: the paradigm may call ``regular_solver(M, ...)`` internally, but the solved model is used internally only and never returned. ``S`` must conform to the standard envelope, and no global or shared state may be modified. **one_step_engine** (required) :: one_step = one_step_engine(S) The *only* input is ``S`` -- the model ``M`` is **not** passed here, so everything the closure needs must have been captured in ``S.solution`` during ``solve_engine``. It returns a closure :: [x_t, retcode, extra...] = one_step(x_tm1, eps_t, regime) with ``x_tm1`` the lagged endogenous vector :math:`x_{t-1}`, ``eps_t`` the shock vector :math:`\varepsilon_t`, and ``regime`` the integer index of the current Markov regime. It returns a nonzero ``retcode`` if the step fails; ``extra...`` are optional outputs such as the local linear coefficients ``(k, H, G)`` consumed by the smoother. **filter_engine** (optional) Future extension for filtering / smoothing. **use_rise_constraint_enforcer** (optional) Logical, default ``true``. Mutable solution objects ======================== For RLL and TP the effective policy function is *state-dependent*: it is updated at each simulation step as the economy evolves. This conflicts with RISE's assumption that the stored solution ``S`` is static. The recommended resolution is to implement ``S.solution`` as a MATLAB **handle class**. Because handle objects are passed by reference, the ``one_step`` closure and RISE's stored copy of ``S.solution`` point to the *same* object: when ``one_step`` updates it in place, RISE's copy reflects the change automatically, without RISE needing to know an update occurred. :: classdef SolutionHandle < handle properties % paradigm-specific fields populated by solve_engine, e.g. T % TP: current polynomial coefficients k, H, G % RLL: current local linear policy % ... end end The workflow is: #. ``solve_engine`` constructs a fresh ``SolutionHandle``, populates it, and stores it as ``S.solution``; #. ``one_step_engine(S)`` extracts ``payload = S.solution`` and builds a closure capturing ``payload`` by reference; #. at each step the closure updates ``payload`` in place; RISE's stored ``S.solution`` reflects the updates automatically; #. RISE reads only the envelope fields and never the payload. .. note:: Because RISE never reads ``S.solution`` directly, this pattern is safe. The only contract is that ``one_step(x_tm1, eps_t, regime)`` always returns a valid ``x_t`` and ``retcode``, whatever the payload's internal state. Responsibilities of RISE ======================== RISE manages model structure and parameter multiplicity; calls ``solve_engine(M, regular_solver)`` for each :math:`\theta_i` (passing ``M`` read-only); stores each :math:`S_i` in ``model.solution(i)``; and routes simulation, forecasting and filtering through the closures returned by ``one_step_engine`` (and ``filter_engine``). The workflow is .. math:: \theta_i \;\rightarrow\; m(\theta_i) \;\rightarrow\; S_i = \texttt{solve\_engine}(m(\theta_i),\,\texttt{regular\_solver}) \;\rightarrow\; \text{store } S_i \;\rightarrow\; \text{simulation / filtering}. RISE must **not** inspect or modify ``S.solution``, assume any format inside it, or reuse a ``one_step`` closure across different parameter vectors. Isolation, safety and persistence ================================== To prevent cross-talk between parameterizations, each call to ``solve_engine`` must construct a **fresh** ``SolutionHandle`` (copying a handle copies only the reference, not the data), no paradigm may keep persistent state outside ``S.solution``, and RISE must not reuse engines across parameter vectors unless the paradigm is stateless. For saving models to disk, paradigms should provide serialization hooks :: blob = serialize_solution(S) S = deserialize_solution(blob) which RISE stores without inspecting. Examples ======== The OccBin paradigm ------------------- The piecewise-linear (Guerrieri–Iacoviello, "OccBin") method for an arbitrary number of occasionally-binding constraints is shipped as a ready-to-use paradigm, ``occbin.paradigm``. The whole method lives in the paradigm package: RISE exchanges only the standard solution envelope and never needs a dedicated occbin option or any in-core occbin state. Write the model as a Markov-switching model with one **reference** regime in which no constraint binds, plus, for each constraint, a chain whose second state is the binding branch (e.g. the policy-rate equation becomes ``R = bind*r_zlb + (1-bind)*R_TAYLOR``). Then activate the paradigm at solve time:: m = dsge_model('mymodel'); C = { struct('var','R','dir',-1,'bound',1,'chain','ocb') }; % R >= 1 m = set(m,'solve_paradigm',{@occbin.paradigm,'ref',1,'constraints',C}); m = solve(m); sims = simulate(m,'simul_periods',1000); RISE stays agnostic about what the paradigm needs: the paradigm itself imposes a single common (unique, imposed) steady state and pins the constraint chains' transition probabilities to zero -- the binding regime is chosen by the constraints, not by chance -- so you do **not** pass ``sstate_imposed`` / ``sstate_unique`` and do **not** set those transition probabilities in the calibration. ``constraints`` is a cell of structs, one per constraint: .. list-table:: :header-rows: 1 :widths: 14 86 * - field - meaning * - ``var`` - endogenous variable carrying the bound (char) * - ``dir`` - ``-1`` ⇒ violated when ``var < bound``; ``+1`` ⇒ violated when ``var > bound`` * - ``bound`` - the numeric bound * - ``chain`` - name of the Markov chain that switches this constraint (char) ``ref`` is the reference regime index (where every chain is in its non-binding state). Further options: ``maxspell`` (default ``200``, the longest binding spell tried before giving up) and ``use_pinv`` (default ``true``). The paradigm enforces the constraint itself, so ``use_rise_constraint_enforcer`` is ``false``. At solve time the paradigm imposes the (common) steady state, evaluates the structural system under each regime, and solves only the reference regime. At simulation time it takes a reference-regime step each period; if a constraint is violated it grows a binding spell (backward recursion with the reference rule as the terminal condition) until the period after the spell is slack again. For several constraints the active set at each date is the set of violated constraints, and the matching regime is read from the model's regime table, so the method scales to any number of chains. There is **no** ``filter_engine``: OccBin has no associated filtering procedure, so estimation/filtering under this paradigm is not provided. The model-side construction (the binding chain and the convex-combination equation) is described in :doc:`/ModelShapes/DSGE/Occasionally-binding constraints`. Recursive local linearization (RLL) ----------------------------------- - ``solve_engine`` calls ``regular_solver`` read-only, extracts the baseline local policy :math:`(k_0, H_0, G_0)`, stores everything in an ``rll.SolutionHandle``, and returns the standard envelope with ``S.solution = payload``. - ``one_step_engine(S)`` returns a closure that builds a zero-shock stance, re-evaluates the structural derivatives at that stance, solves the local linear system for updated :math:`(k_t, H_t, G_t)`, updates the payload in place, and returns :math:`x_t` together with :math:`(k_t, H_t, G_t)` for the smoother. :math:`k`-th order Taylor projection (TP) ----------------------------------------- - ``solve_engine`` calls ``regular_solver`` read-only, uses the perturbation solution as a warm start, runs ``rtp.solve`` order by order to obtain :math:`T^* = \{T_1^*,\dots,T_h^*\}`, and stores :math:`T^*`, the topology and ``get_F`` in an ``rtp.SolutionHandle``. The solved coefficients are converted to divided differences (Taylor coefficients) before they are stored, so that .. math:: x_t = \sum_{q=0}^{K} T^*_{\text{regime}}\{q{+}1\}\, \bigl(z_t - z_{\text{anchor}}\bigr)^{\otimes q}. - ``one_step_engine(S)`` returns a closure with two schemes, selected by the ``reanchor`` option: - **re-anchored** (``reanchor=true``, the default — Levintal's scheme): at each step it rebuilds ``get_F`` with the anchor at the *current* state :math:`x_{t-1}` (``rtp.from_rise(m, x_{t-1}, opts)``, which evaluates the model derivatives along the trajectory, with :math:`x_{t+1}=g(\text{forward state})`), re-solves ``rtp.solve`` there, and evaluates the polynomial. This re-centers the approximation on the current state and is what makes TP *more accurate than perturbation away from the steady state*, at the cost of a projection re-solve per step. - **fixed polynomial** (``reanchor=false``): evaluates the stored steady-state anchored :math:`T^*` directly. Fast, but at the steady-state anchor this coincides with perturbation (single-anchor TP and perturbation impose the same conditions there). In both cases :math:`z_t = (p_{t-1}, b_{t-1}, \sigma{=}1, \varepsilon_t)` and :math:`z_{\text{anchor}} = (p_{t-1}, b_{t-1}, \sigma{=}0, \varepsilon{=}0)`. Solution accuracy can be quantified with the model's :ref:`accuracy method `; on the RBC model re-anchored TP attains an Euler error 8--13 times smaller than perturbation at :math:`\pm 30\%` capital. Risk-adjusted linear (RAL) -------------------------- The risk-adjusted linear paradigm, ``ral.paradigm``, turns a higher-order perturbation solution into a *linear* one that still carries the effect of uncertainty. ``solve_engine`` calls ``regular_solver`` read-only at an arbitrary internal order :math:`k` (the user's choice), then takes the **linearization** of that policy function: it keeps every term that fits a linear representation in the states and shocks -- the constant (the precautionary :math:`\sigma^a` level shifts) and the linear and risk-adjusted slope terms -- and discards the genuine curvature (:math:`\text{state}\times\text{state}`, :math:`\text{state}\times\text{shock}`, ...) and every higher-order block. The result is the affine policy .. math:: x_t = \bar{x} + c + A\,(x_{t-1}-\bar{x}) + B\,\varepsilon_t, where :math:`c` is the precautionary level correction and :math:`A, B` are the risk-adjusted slopes. Under regime switching the linearization is taken per regime, so each regime gets its own precautionary level and slopes. Unlike the other paradigms, RAL **dissolves** after solving. Because its output is a genuine first-order solution, ``solve_engine`` hands the native ``state_space`` back to RISE (signalling ``S.dissolve = true`` in the envelope) and steps aside: the dispatcher adopts the native solution and clears the paradigm, so from then on the model is an ordinary first-order model. Every downstream path -- ``simulate``, ``irf``, and in particular the analytic linear Kalman filter that RISE auto-selects when no filter is named -- runs natively, with no bespoke RAL ``one_step`` or filter. The precautionary level is carried in the sigma (perturbation) column of the first-order solution, so it enters simulation and the filter constant and correctly cancels out of impulse responses (which are deviations). Activate it at solve time:: m = dsge_model('mymodel','max_deriv_order',2); m = set(m,'solve_paradigm',{@ral.paradigm,'order',2}); m = solve(m); % m is now a native first-order model sims = simulate(m,'simul_periods',1000); The ``order`` option (default 2) is the internal perturbation order from which the risk correction is read: ``order 2`` captures the leading precautionary level and the risk-adjusted slopes; higher orders additionally fold in the :math:`\sigma^a` (:math:`a>2`) corrections. The output is first order regardless. Evaluation is first-order cheap and needs no pruning -- the only nonlinear content, the curvature, has been removed -- and the residual gap to the full order-:math:`k` solution is exactly that discarded curvature. Design principles ================= - A model has exactly one paradigm. - A model may have multiple parameterizations; each has one solution ``S``. - ``M`` is always read-only inside a paradigm. - ``S.solution`` is always opaque to RISE. - The envelope gives RISE just enough to orchestrate workflows. - Mutable simulation-time state lives inside ``S.solution`` via handle semantics. - Paradigm-specific solution concepts (eigenvalues, number of solutions, Taylor coefficients) live inside ``S.solution``, not in the envelope. - A *dissolving* paradigm (e.g. ``ral.paradigm``) is the one exception to the first and fourth points: when its result is itself a standard solution it sets ``S.dissolve = true`` and hands back a native ``state_space``; the dispatcher adopts it and clears the paradigm, leaving a paradigm-free model that RISE filters and simulates with its ordinary machinery.